package com.elnguage.lox;

import static com.elnguage.lox.TokenType.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


public class Scanner{
    private final String source;
    private int start;
    private int current;
    private int line;
    private final List<Token> tokens = new ArrayList<>();
    private static final Map<String, TokenType> keywords;
    static 
    {
        keywords = new HashMap<>();
        keywords.put("and", AND);
        keywords.put("or", OR);
        keywords.put("class", CLASS);
        keywords.put("else", ELSE);
        keywords.put("false", FALSE);
        keywords.put("for", FOR);
        keywords.put("fun", FUN);
        keywords.put("nil", NIL);
        keywords.put("print", PRINT);
        keywords.put("reutrn", RETURN);
        keywords.put("super", SUPER);
        keywords.put("this", THIS);
        keywords.put("true", TRUE);
        keywords.put("var", VAR);
        keywords.put("while", WHILE);
        keywords.put("stellaris_event", STELLA_EVENT);
        keywords.put("stellaris_handle", STELLA_HANDLE);
    }

    Scanner(String source)
    {
        this.source = source;
        start = 0;
        current = 0;
        line = 1;
    }
    
    List<Token> scanTokens()
    {
        while(!isAtEnd())
        {
            start = current;
            scanToken();
        }

        tokens.add(new Token(EOF, "", null, line));
        return tokens;
    }
    
    boolean isAtEnd()
    {
        return current >= source.length();
    }

    private void scanToken()
    {
        char c = advance();
        switch(c)
        {
            case '(':
                addToken(LEFT_PAREN);
                break;
            case ')':
                addToken(RIGHT_PAREN);
                break;
            case '{':
                addToken(LEFT_BRACE);
                break;
            case '}':
                addToken(RIGHT_BRACE);
                break;
            case '+':
                addToken(PLUS);
                break;
            case '-':
                if(match('>'))
                {
                    addToken(STELLA_EVENT_CAT);
                }
                else
                {
                    addToken(MINUS);
                }
                break;
            case '*':
                addToken(STAR);
                break;
            case ',':
                addToken(COMMA);
                break;
            case '.':
                addToken(DOT);
                break;
            case ';':
                addToken(SEMICOLON);
                break;
            case '=':
                if(match('='))
                {
                    addToken(EQUAL_EQUAL);
                }   
                else
                {
                    addToken(EQUAL);
                }
                break;
            case '>':
                if(match('='))
                {
                    addToken(GREATER_EQUAL);   
                }
                else
                {
                    addToken(GREATER);
                }
                break;
            case '<':
                if(match('='))
                {
                    addToken(LESS_EQUAL);
                }
                else
                {
                    addToken(LESS);
                }
                break;
            case '!':
                if(match('='))
                {
                    addToken(BANG_EQUAL);
                }
                else
                {
                    addToken(BANG);
                }
                break;
            case '/':
                if(match('/'))
                {
                    while(!isAtEnd() && peek() != '\n')
                    {
                        advance();                        
                    }
                }
                else
                {
                    addToken(SLASH);
                }
                break;
               
            //ignore these
            case ' ':
                break;
            case '\n':
                line++;
                break;
            case '\t':
                break;
            case '\r':
                break;
            case '"':
                string();
                break;
            default:

                if(isDigital(c))
                {
                    number();
                }
                else if(isAlpha(c))
                {
                    identifier();
                }
                else
                {
                    Lox.error(line, "Parser error: Unexpected character");
                }
                break;
        }
    }
    private void string()
    {
        while(!isAtEnd() && peek() != '"')
        {
            if(peek() == '\n')  //allow multiple lines of string
                line++;
            advance();
        }
        if(isAtEnd())
        {
            Lox.error(line, "Parser: String is not embraced by a pair of double quotes");
        }

        //advance the right "
        advance();

        String literalValue = source.substring(start + 1, current - 1); //+1, -1!
        addToken(STRING, literalValue);
    }

    private void number()
    {
        while(isDigital(peek())) advance();

        if(peek() == '.' && isDigital(peekNext()))
        {
            advance();
            while(isDigital(peek())) advance();
        }

        try
        {
            addToken(NUMBER, Double.parseDouble(source.substring(start, current)));
        } 
        catch (Exception e) {
            Lox.error(line, "Parser: Can't parse number");
        }
    }

    private void identifier()
    {
        while(isAlpha(peek()) || isDigital(peek()))  advance();
        String toJudge = source.substring(start, current);
        TokenType type = keywords.get(toJudge);
        if(type != null)
        {
            addToken(type);
        }

        else
        {
            if(toJudge.equals("ocp"))
            {
                addToken(STELLA_OCP);
            }
            addToken(IDENTIFIER);
        }
    }


    private boolean isDigital(char c)
    {
        return (c >= '0' && c<= '9');
    }

    private boolean isAlpha(char c)
    {
        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
    }

    private char peek()
    {
        if(isAtEnd()) return '\0';          //really get there?
        return source.charAt(current);
    }

    private char peekNext()
    {
        current++;
        if(isAtEnd()) return '\0';
        current--;
        return source.charAt(current + 1);
    }

    private boolean match(char expected)
    {
        if(isAtEnd())   return false;
        if(source.charAt(current) != expected) return false;
        current++;
        return true;
    }
    private char advance()
    {
        current++;
        return source.charAt(current-1);
    }
    private void addToken(TokenType type)
    {
        addToken(type, null);
    }
    private void addToken(TokenType type, Object literal)
    {
        String text = source.substring(start, current);
        tokens.add(new Token(type, text, literal, line));
    }
}